Перейти к основному содержимому

5.08. Рекомендации по разработке на Smalltalk

Разработчику Архитектору

Рекомендации по разработке на Smalltalk

Smalltalk представляет собой уникальную среду программирования с образной моделью выполнения, динамической типизацией и философией «всё есть объект». Эти особенности формируют специфический подход к написанию кода. Рекомендации в этой главе помогают создавать читаемый, поддерживаемый и идиоматичный код на Smalltalk, соответствующий многолетним традициям языка.

Именование сущностей

Классы и пакеты

Имена классов начинаются с заглавной буквы и используют стиль PascalCase. Каждое слово в составном имени начинается с заглавной буквы без разделителей.

Object
Collection
OrderedCollection
Dictionary
FileStream

Имена классов выражают сущность или роль объекта в системе. Предпочтительны существительные или словосочетания с существительными. Избегайте глагольных форм в именах классов.

"Хорошо"
Customer
BankAccount
TransactionProcessor

"Плохо"
ProcessTransaction
HandleData

Пакеты (или категории классов) организуют пространство имен. Их имена используют точечную нотацию с заглавными буквами для каждого компонента.

Collections
Collections-Sequenceable
Kernel-Objects
Morphic-Core

Методы и сообщения

Имена методов начинаются со строчной буквы и используют стиль camelCase. Smalltalk различает три типа сообщений, каждый со своими правилами именования.

Унарные сообщения не принимают аргументов. Имя представляет действие или свойство объекта.

size
isEmpty
asArray
printString

Бинарные сообщения принимают один аргумент и используют специальные символы. Стандартные бинарные селекторы включают арифметические и логические операторы.

+
-
*
/
=
~
>
<
>=
<=

Ключевые сообщения принимают один или несколько аргументов. Каждый аргумент сопровождается ключевым словом, заканчивающимся двоеточием. Ключевые слова вместе образуют осмысленную фразу.

at: 1
at: 1 put: 'value'
between: 1 and: 10
indexOf: $a startingAt: 5

Имена ключевых сообщений читаются как предложения. Каждое ключевое слово начинается со строчной буквы, содержит осмысленную часть глагола или предлога, завершается двоеточием.

"Хорошо"
add: anObject
removeKey: aKey ifAbsent: aBlock
copyFrom: start to: stop

"Плохо"
addElement:
rmvKey:

Переменные

Инстанс-переменные именуются в стиле camelCase со строчной буквы. Имя отражает роль или назначение состояния объекта.

name
accountBalance
transactionHistory

Локальные переменные и параметры методов также используют camelCase со строчной буквы. Имена должны быть достаточно описательными для понимания контекста без избыточности.

"Хорошо"
index
element
collection
totalAmount

"Плохо"
i
x
temp
data1

Для параметров, представляющих объекты определённого класса, допустимо использовать префикс, указывающий на тип.

aCustomer
anAccount
someItems
newName

Временные переменные в блоках следуют тем же правилам. Для простых итераций допустимы короткие имена, если контекст очевиден.

collection do: [ :each | each doSomething ]
numbers collect: [ :n | n squared ]

Глобальные переменные начинаются с заглавной буквы. Их использование ограничено системными объектами и точками входа в систему.

Object
Smalltalk
Transcript

Создание собственных глобальных переменных не рекомендуется. Предпочтительна организация через классы и синглтоны.

Структура и организация кода

Категории классов и методов

Система категорий обеспечивает логическую группировку классов и методов. Каждый класс принадлежит одной категории классов. Методы внутри класса организуются в категории методов.

Категории классов отражают функциональную область:

Collections
Collections-Abstract
Collections-Sequenceable
Kernel-Methods
Graphics-Display

Категории методов группируют методы по протоколу или назначению:

"Пример категорий методов в классе Collection"
accessing "Методы получения элементов"
adding "Методы добавления элементов"
removing "Методы удаления элементов"
enumerating "Методы итерации"
comparing "Методы сравнения"
private "Внутренние методы реализации"

Размещение метода в правильной категории упрощает навигацию и понимание интерфейса класса.

Протоколы методов

Протокол определяет набор сообщений, которые объект обещает понимать. Явное следование протоколам повышает предсказуемость кода.

Стандартные протоколы Smalltalk включают:

  • accessing — получение и установка состояния
  • comparing — сравнение объектов
  • copying — создание копий
  • testing — проверка состояния (возвращают булевы значения)
  • converting — преобразование в другие типы
  • enumerating — итерация по коллекциям
  • initialization — инициализация объектов

Методы тестирования именуются с префиксом is, has, can и возвращают булево значение.

isEmpty
isNil
hasChildren
canBeModified

Методы преобразования начинаются с as и возвращают новый объект другого типа.

asArray
asString
asInteger
asSet

Методы копирования начинаются с copy или clone.

copy
deepCopy
shallowCopy

Структура файлов в современных реализациях

Современные среды Smalltalk (Pharo, Squeak) поддерживают экспорт кода в файловую систему через форматы Tonel или FileTree. Структура каталогов отражает организацию пакетов.

src/
Collections/
Collection.class.st
SequenceableCollection.class.st
Kernel/
Object.class.st
Class.class.st
MyApplication/
Customer.class.st
BankAccount.class.st
Transaction.class.st

Каждый класс сохраняется в отдельном файле с расширением .class.st. Методы класса и инстанс-методы хранятся в разных секциях файла.

"Пример файла Customer.class.st"
Class {
#name : #Customer,
#superclass : #Object,
#instVars : [ 'name', 'email', 'accounts' ],
#category : #'MyApplication-Customers'
}

{ #category : #accessing }
Customer >> name [
^ name
]

{ #category : #accessing }
Customer >> name: aString [
name := aString
]

{ #category : #adding }
Customer >> addAccount: aBankAccount [
accounts add: aBankAccount
]

Оформление кода

Отступы и пробелы

Используйте два пробела для каждого уровня вложенности. Табуляция не применяется.

"Хорошо"
calculateTotal: items [
| total |
total := 0.
items do: [ :each |
total := total + each price.
].
^ total
]

"Плохо — смешанные отступы"
calculateTotal: items [
| total |
total := 0.
items do: [ :each |
total := total + each price.
].
^ total
]

Пробелы разделяют элементы выражений для улучшения читаемости:

  • Один пробел вокруг бинарных операторов
  • Один пробел после запятой в списках
  • Один пробел между ключевым словом и аргументом в ключевых сообщениях
  • Отсутствие пробела перед двоеточием в ключевых сообщениях
"Хорошо"
total := (a + b) * (c - d).
names := #('Alice' 'Bob' 'Charlie').
result := collection at: index put: value.

"Плохо"
total:=(a+b)*(c-d).
names:=#('Alice','Bob','Charlie').
result:=collection at :index put :value.

Длина строк и разбиение выражений

Максимальная длина строки составляет 80 символов. Длинные выражения разбиваются на несколько строк с выравниванием по открывающей скобке или оператору.

"Хорошо"
result := aCollection
select: [ :each | each isRelevant ]
thenCollect: [ :each | each transformedValue ].

configuration := Dictionary new
at: #host put: 'localhost';
at: #port put: 8080;
at: #timeout put: 30;
yourself.

Бинарные сообщения размещаются в начале строки при переносе.

sum := (1 to: 100)
+ (200 to: 300)
+ (400 to: 500).

Блоки кода

Блоки кода с одним аргументом используют :each или осмысленное имя аргумента.

collection do: [ :each | each process ].
numbers select: [ :number | number isPrime ].

Блоки с несколькими аргументами именуют каждый аргумент осмысленно.

dictionary keysAndValuesDo: [ :key :value |
Transcript show: key; show: ' -> '; show: value; cr
].

Пустые блоки допустимы только при явной необходимости игнорирования значения.

[ ] "Пустой блок"
[ :ignored | ] "Блок с игнорированием аргумента"

Скобки и группировка

Скобки группируют выражения для управления порядком вычислений. Избыточные скобки, не влияющие на порядок, опускаются.

"Хорошо"
result := a + b * c.
result := (a + b) * c.

"Плохо — избыточные скобки"
result := ((a) + (b)).

Проектирование классов

Принцип единственной ответственности

Каждый класс решает одну задачу. Класс управляет одним аспектом поведения или представляет одну сущность предметной области.

"Хорошо — разделение ответственности"
Customer "представляет клиента"
BankAccount "управляет счетом"
Transaction "представляет операцию"
TransactionProcessor "обрабатывает операции"

Классы с именами, содержащими союз «и» или перечисление обязанностей, нарушают принцип единственной ответственности.

"Плохо"
CustomerAndAccountManager
DataProcessorAndValidator

Инкапсуляция состояния

Состояние объекта доступно только через методы доступа. Прямой доступ к инстанс-переменным извне класса запрещён.

"Хорошо"
Customer >> name [
^ name
]

Customer >> name: aString [
name := aString
]

"Плохо — прямой доступ к инстанс-переменной из другого класса"
anotherObject customer name: 'New Name'. "Допустимо"
anotherObject customer instVarNamed: 'name' put: 'New Name'. "Недопустимо"

Инстанс-переменные объявляются в классе и инициализируются в методе initialize.

Customer class >> new [
^ super new initialize
]

Customer >> initialize [
super initialize.
name := ''.
email := ''.
accounts := OrderedCollection new.
]

Наследование и композиция

Наследование применяется для специализации поведения в иерархиях «является». Композиция предпочтительна для отношений «имеет» или «использует».

"Наследование — правильно"
Animal
^-- Mammal
^-- Dog
^-- Cat

"Композиция — правильно"
Car has: Engine
Car has: Wheel

Избегайте глубоких иерархий наследования. Предпочтительна плоская структура с композицией и делегированием.

"Плохо — глубокая иерархия"
Object
^-- Collection
^-- SequenceableCollection
^-- ArrayedCollection
^-- Array
^-- ByteArray
^-- Bitmap

Классовые методы и инстанс-методы

Классовые методы создают экземпляры или предоставляют функциональность уровня класса. Инстанс-методы определяют поведение отдельных объектов.

"Классовые методы — создание экземпляров"
Date today
Point x: 10 y: 20
Array with: 1 with: 2 with: 3

"Инстанс-методы — поведение объекта"
aPoint x
aDate dayOfMonth
anArray at: 1

Классовые переменные используются редко, только для состояния, общего для всех экземпляров класса.

Проектирование методов

Длина и сложность методов

Метод выполняет одну задачу и помещается на экране без прокрутки. Оптимальная длина — до 10 строк. Методы длиннее 20 строк требуют декомпозиции.

"Хорошо — короткий метод"
Customer >> fullName [
^ firstName, ' ', lastName
]

"Требует декомпозиции"
processTransaction: aTransaction [
| amount account balance newBalance |
amount := aTransaction amount.
account := self accountFor: aTransaction accountNumber.
balance := account balance.
newBalance := balance + amount.
account balance: newBalance.
self logTransaction: aTransaction withBalance: newBalance.
self notifyObserversOf: aTransaction.
^ newBalance
]

"Декомпозированная версия"
processTransaction: aTransaction [
| account |
account := self accountFor: aTransaction accountNumber.
^ self
updateBalanceOf: account with: aTransaction amount;
logAndNotify: aTransaction for: account
]

updateBalanceOf: anAccount with: anAmount [
^ anAccount balance: anAccount balance + anAmount
]

logAndNotify: aTransaction for: anAccount [
self logTransaction: aTransaction withBalance: anAccount balance.
self notifyObserversOf: aTransaction.
^ anAccount balance
]

Параметры методов

Методы принимают минимально необходимое количество параметров. Предпочтительны методы без параметров или с одним параметром. Методы с тремя и более параметрами требуют пересмотра дизайна.

"Хорошо"
collection add: anObject.
date between: startDate and: endDate.

"Требует улучшения"
processor process: data withOptions: options andContext: context andLogger: logger.

Для методов с множеством параметров применяется объект параметров или построитель.

"Объект параметров"
options := ProcessingOptions new
timeout: 30;
retries: 3;
logLevel: #debug;
yourself.
processor process: data with: options.

Возврат значений

Метод возвращает значение, соответствующее его назначению. Методы доступа возвращают запрашиваемое состояние. Методы-модификаторы часто возвращают получателя сообщения для поддержки каскадирования.

"Метод доступа"
collection size. "возвращает число"

"Метод-модификатор с возвратом получателя"
builder
add: item1;
add: item2;
build. "возвращает результат построения"

Избегайте методов, возвращающих nil в ситуациях, когда ожидается коллекция или значение. Возвращайте пустую коллекцию вместо nil.

"Хорошо"
customer transactions. "возвращает пустую коллекцию если нет транзакций"

"Плохо"
customer transactions. "возвращает nil если нет транзакций"

Обработка ошибок

Исключения применяются для обработки исключительных ситуаций. Проверка условий предпочтительна перед отправкой сообщения, которое может вызвать исключение.

"Проверка перед действием"
aCollection ifNotEmpty: [ aCollection first ].

"Перехват исключения"
[ aCollection first ]
on: Error
do: [ :ex | self defaultItem ].

Создавайте специфичные классы исключений для предметной области.

InsufficientFundsException new signal.
InvalidTransactionException new signal.

Избегайте пустых блоков on:do: без логирования или восстановления состояния.

Архитектурные подходы

Модель-Представление-Контроллер

Smalltalk является родоначальником паттерна MVC. Архитектура разделяет:

  • Модель — предметную область и бизнес-логику
  • Представление — отображение состояния модели
  • Контроллер — обработку пользовательского ввода

Модель не зависит от представления и контроллера. Представление наблюдает за моделью через механизм зависимостей.

"Модель"
BankAccount >> deposit: anAmount [
balance := balance + anAmount.
self changed: #balance.
]

"Представление регистрирует зависимость"
account addDependent: balanceDisplay.

Коллекции и перебор

Smalltalk предоставляет богатую иерархию коллекций. Используйте специализированные коллекции вместо универсальных.

Set "уникальные элементы без порядка"
OrderedCollection "упорядоченные элементы с быстрым доступом по индексу"
Dictionary "ассоциативный массив"
SortedCollection "автоматически сортируемая коллекция"

Предпочитайте методы перебора (do:, collect:, select:, detect:) императивным циклам.

"Хорошо — функциональный стиль"
names := people collect: [ :each | each name ].
adults := people select: [ :each | each age >= 18 ].
total := items inject: 0 into: [ :sum :each | sum + each price ].

"Плохо — императивный стиль"
names := OrderedCollection new.
people do: [ :each | names add: each name ].

adults := OrderedCollection new.
people do: [ :each | (each age >= 18) ifTrue: [ adults add: each ] ].

total := 0.
items do: [ :each | total := total + each price ].

Блоки как объекты первого класса

Блоки кода являются полноценными объектами. Их можно передавать как аргументы, возвращать из методов, сохранять в переменных.

"Передача блока как аргумента"
collection do: [ :each | each process ].

"Возврат блока из метода"
handlerFor: anEvent [
anEvent = #click ifTrue: [ ^ [ :target | target handleClick ] ].
anEvent = #hover ifTrue: [ ^ [ :target | target showTooltip ] ].
^ [ :target | ]
]

"Сохранение блока в переменной"
action := [ :value | value squared ].
result := action value: 5.

Комментарии и документация

Виды комментариев

Однострочные комментарии начинаются с двойного кавычка. Многострочные комментарии используют тот же синтаксис, продолжаясь на нескольких строках.

"Однострочный комментарий"

"Многострочный комментарий
продолжается на следующей строке
и завершается закрывающим кавычком"

Комментарии размещаются над комментируемым кодом или в конце строки для кратких пояснений.

"Вычисляем итоговую сумму с учётом скидки"
total := self calculateTotalWithDiscount.

index := 1. "Начинаем с первого элемента"

Документация классов

Класс сопровождается комментарием, описывающим его назначение, ответственность и примеры использования.

"
Customer представляет клиента банка.

Управляет личной информацией клиента и коллекцией банковских счетов.
Поддерживает добавление и удаление счетов, получение общей суммы по всем счетам.

Примеры использования:
customer := Customer new.
customer name: 'Иван Петров'.
customer addAccount: (BankAccount new).

total := customer totalBalance.
"
Class {
#name : #Customer,
#superclass : #Object,
#instVars : [ 'name', 'accounts' ],
#category : #'Banking-Model'
}

Документация методов

Методы сопровождаются комментарием, описывающим их поведение, параметры и возвращаемое значение. Комментарий размещается непосредственно перед методом.

"
Возвращает полное имя клиента в формате 'Фамилия Имя'.

Имя и фамилия объединяются пробелом. Если одно из значений отсутствует,
возвращается доступная часть имени.

Пример:
customer firstName: 'Иван'; lastName: 'Петров'.
customer fullName. "возвращает 'Петров Иван'"
"
fullName [
^ String streamContents: [ :stream |
lastName ifNotNil: [ stream nextPutAll: lastName ].
(firstName notNil and: [ lastName notNil ]) ifTrue: [ stream space ].
firstName ifNotNil: [ stream nextPutAll: firstName ]
]
]

Избегайте комментариев, повторяющих код. Комментарий объясняет «почему», а не «что» делает код.

"Плохо — комментарий повторяет код"
index := index + 1. "Увеличиваем индекс на единицу"

"Хорошо — комментарий объясняет причину"
"Пропускаем первый элемент, так как он содержит заголовок"
index := 2.

Тестирование

Структура тестовых классов

Тестовые классы наследуются от TestCase. Имя тестового класса заканчивается на Test.

CustomerTest >> testFullName [
| customer |
customer := Customer new.
customer firstName: 'Иван'; lastName: 'Петров'.
self assert: customer fullName equals: 'Петров Иван'
]

CustomerTest >> testFullNameWithMissingLastName [
| customer |
customer := Customer new.
customer firstName: 'Иван'.
self assert: customer fullName equals: 'Иван'
]

Каждый тестовый метод начинается с префикса test и проверяет один аспект поведения.

Организация тестов

Тесты группируются по функциональности в отдельные методы. Используйте методы setUp и tearDown для подготовки и очистки окружения.

CustomerTest >> setUp [
super setUp.
customer := Customer new.
customer firstName: 'Иван'; lastName: 'Петров'
]

CustomerTest >> testFullName [
self assert: customer fullName equals: 'Петров Иван'
]

CustomerTest >> testAddingAccount [
| account |
account := BankAccount new.
customer addAccount: account.
self assert: customer accounts size equals: 1
]

Предпочитайте конкретные проверки (assert:equals:, assert:identicalTo:) общим (assert:).

"Хорошо"
self assert: result equals: expectedValue.
self assert: object identicalTo: expectedObject.

"Плохо"
self assert: result = expectedValue.

Идиомы и практики Smalltalk

Отправка сообщений вместо вызова методов

В Smalltalk объекты взаимодействуют через отправку сообщений. Получатель решает, как обработать сообщение. Эта модель обеспечивает гибкость и полиморфизм.

"Отправка унарного сообщения"
collection size.

"Отправка бинарного сообщения"
5 + 3.

"Отправка ключевого сообщения"
dictionary at: #key put: 'value'.

Проверка возможности обработки сообщения выполняется через respondsTo: или перехват исключения MessageNotUnderstood.

(anObject respondsTo: #name) ifTrue: [ anObject name ].

Самодокументируемый код

Код на Smalltalk стремится быть самодокументируемым через осмысленные имена и структуру. Длинные имена методов допустимы, если они точно описывают поведение.

"Хорошо — имя метода раскрывает поведение"
customer hasSufficientFundsFor: anAmount.
transaction isAuthorizedBy: aManager.
date fallsBetween: startDate and: endDate.

Избегайте сокращений, кроме общепринятых в предметной области.

"Хорошо"
url
http
json
xml

"Плохо"
usr
pwd
cnt
tmp

Каскадирование сообщений

Каскадирование позволяет отправить несколько сообщений одному получателю без повторного указания имени.

"Без каскадирования"
builder add: item1.
builder add: item2.
builder build.

"С каскадированием"
builder
add: item1;
add: item2;
build.

Каскадирование улучшает читаемость при настройке объектов или построении сложных структур.

Отладка в образе

Среда выполнения Smalltalk предоставляет мощные инструменты отладки:

  • Инспектор для просмотра состояния объекта
  • Отладчик для пошагового выполнения
  • Варианты продолжения выполнения после ошибки
  • Возможность изменения кода во время выполнения

При возникновении исключения отладчик предлагает варианты продолжения:

  • Proceed — продолжить выполнение
  • Debug — войти в отладчик
  • Close — завершить выполнение метода
  • Restart — перезапустить метод

Изменения кода в отладчике применяются немедленно без перекомпиляции всей системы.

Версионный контроль

Форматы хранения кода

Современные реализации Smalltalk используют форматы для интеграции с системами контроля версий:

  • Tonel — человекочитаемый формат, рекомендуемый для новых проектов
  • FileTree — формат на основе файловой структуры
  • Monticello — исторический формат пакетов

Структура проекта в формате Tonel:

repository/
.properties
package1/
class1.class.st
class2.class.st
package1.package.st
package2/
class3.class.st
package2.package.st

Файл .properties содержит метаданные репозитория. Файлы .package.st описывают пакеты.

Рабочий процесс с Git

  1. Экспортируйте образ в файловую систему через выбранный формат
  2. Инициализируйте Git-репозиторий в корневом каталоге
  3. Фиксируйте изменения файлов классов и пакетов
  4. При работе в команде импортируйте изменения из репозитория в образ
"Экспорт в файловую систему"
Metacello new
baseline: 'MyProject';
repository: 'tonel://./src';
load.

"Импорт изменений из файловой системы"
Metacello new
baseline: 'MyProject';
repository: 'tonel://./src';
load.

Разделяйте логические изменения на отдельные коммиты. Каждый коммит должен представлять законченную функциональность или исправление.